/**
 * Copyright (C) 2009-2012, KingHool.com
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see http://www.gnu.org/licenses.
 */
package com.kinghool.smodel;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.jdom.Attribute;
import org.jdom.CDATA;
import org.jdom.Comment;
import org.jdom.Content;
import org.jdom.DocType;
import org.jdom.Element;
import org.jdom.Namespace;
import org.jdom.filter.Filter;

import com.kinghool.ebos.commons.utils.Assert;
import com.kinghool.ebos.commons.utils.Utils;
import com.kinghool.smodel.def.TreeModelDefinition;
import com.kinghool.smodel.def.XmlModelDefinition;
import com.kinghool.smodel.query.Query;
import com.kinghool.smodel.utils.JDomUtils;

/**
 * 
 * @Author Allen Deng(dengtailin@gmail.com) 
 */
public class XmlModel extends TreeModel implements IXmlModel {
	protected Element element;
	protected String text;
	protected String namespacePrefix;
	protected String namespaceUri;
	protected Map<String, Map<String, String>> attributeNameSpace = Utils.newMap();

	protected DocType docType;

	public XmlModel(XmlModelDefinition modelDefinition) {
		this(modelDefinition, null);
	}
 
	public XmlModel(XmlModelDefinition modelDefinition, IXmlModel parent) {
		this(new Element(modelDefinition.getId()), parent, modelDefinition);
	}
	
	public XmlModel(String id) {
		this(id, null);
	}
	
	public XmlModel(String id, IXmlModel parent) {
		this(new Element(id), parent, XmlModelDefinition.createDefaultXmlModelDefinition(id, parent == null ? null : (XmlModelDefinition) parent.getModelDefinition()));
	}
	
	public XmlModel(Element element, IXmlModel parent, XmlModelDefinition modelDefinition) {
		super(parent, modelDefinition);
		
		if (element != null) {
			this.element = element;
		}
		
		initModel(element);
	}
	
	public XmlModel(Element element, IXmlModel parent) {
		this(element, parent, XmlModelDefinition.createDefaultXmlModelDefinition(element.getName(), 
				                                         parent == null ? null : (XmlModelDefinition) parent.getModelDefinition()));
	}
	
	@SuppressWarnings("unchecked")
	protected void initModel(Element element) { 
		if (element != null) {  
			List<Attribute> attributes = element.getAttributes();
			for (Attribute attribute : attributes) {
				installProperty(attribute.getName(), attribute.getValue());
				if (! Assert.isNullOrEmpty(attribute.getNamespaceURI())) {
					setPropertyNameSpace(attribute.getName(), 
							                 attribute.getNamespacePrefix(), 
							                 attribute.getNamespaceURI());
				}
			}
			 
			if (element.getNamespace() != null) {
				setNamespacePrefix(element.getNamespacePrefix());
				setNamespaceUri(element.getNamespaceURI());
			}
			
			List<Element> childrenElement = element.getChildren();
			for (Element childElement : childrenElement) {  
				XmlModelDefinition childDefinition = XmlModelDefinition.getNotNullChildXmlModelDefinition(childElement.getName(), getModelDefinition());
				XmlModel childXmlModel = new XmlModel(childElement, this, childDefinition);			 
				installChild(childXmlModel); 	 
			}
			this.element = element;
			
			this.text = element.getText();
 			setModelId(element.getName());
		}
	} 
	
	public XmlModelDefinition getModelDefinition() {
		return (XmlModelDefinition) modelDefinition;
	}  
	
	public IXmlModel addChild(String childFullName, String textValue, Map<String, ? extends Object> propValPairs) {
		childFullName = formatName(childFullName);
		IXmlModel child = (IXmlModel) super.addChild(childFullName, propValPairs);
		 
		if (child != null && Assert.isNotNullNotEmpty(textValue)) {
			child.setText(textValue);
		}
		return child;
	} 
	
	protected ITreeModel createChild(ITreeModel parent, String childName, TreeModelDefinition modelDefinition) {
        childName = this.formatName(childName);
		Element childElement = new Element(childName);
		XmlModelDefinition childModelDef = XmlModelDefinition.getNotNullChildXmlModelDefinition(childName, (XmlModelDefinition) modelDefinition); 
		if (childModelDef != null) {
             childElement = childModelDef.createDefaultElement(childName, false);
		}  
		IXmlModel createdChild = new XmlModel(childElement, (IXmlModel) parent, childModelDef);	 
		parent.addChild(createdChild); 
		
		return createdChild;
	}
	
	protected TreeModelDefinition getNotNullChildModelDefinition(String childId, TreeModelDefinition modelDefinition) {
		return XmlModelDefinition.getNotNullChildXmlModelDefinition(childId, (XmlModelDefinition)modelDefinition);
	}
	
	public IXmlModel addChild(String childFullName, String textValue) {
		childFullName = formatName(childFullName);
		return addChild(childFullName, textValue, new HashMap<String, Object>());
	} 
	
	protected boolean isBlank() {
		return super.isBlank() && Assert.isNullOrEmpty(getText());
	} 

	public String getChildText(String childFullName, Query... queries) {
		childFullName = formatName(childFullName);
		ITreeModel child = getFirstTreeChild(childFullName, queries);
		if (child instanceof IXmlModel) {
			return Utils.getNotNullAndTrimString(((IXmlModel)child).getText());
		}
		return null;
	}
 
	public void setChildText(String childFullName, String textValue, Query... queries) {
		childFullName = this.formatName(childFullName);
		
		if ((Assert.isNullOrEmpty(textValue) && Assert.isNullOrEmpty(getChildText(childFullName, queries))
			|| (! Assert.isNullOrEmpty(textValue) && textValue.equals(getChildText(childFullName, queries))))) {
			//the textValue is equal to the model, no need to set.
			return;
		} 
		
		IXmlModel child = (IXmlModel) createChild(childFullName, getModelDefinition(), this, false, queries);
		if (child != null) {
			child.setText(textValue);
		}
	} 

	public Map<String, String> getPropertyNameSpace(String property) {
		return attributeNameSpace.get(property);
	}

	public void setPropertyNameSpace(String property, String prefix, String uri) {
		Map<String, String> nameSpace = new HashMap<String, String>();
		nameSpace.put(uri, prefix);
		attributeNameSpace.put(property, nameSpace);
	}

	public Element populate() {
		if (element == null) {
			element = new Element(getModelId());
		}
		populateToElement(element, this);
		
		return element;
	}

	public Element getElement() {
		if (element == null) {
			element = new Element(getModelId());
		}
		return element;
	}

	public void refresh() throws Exception { 
		reset();
		initModel(element);
	}
	
	public void refresh(Element element) {
		reset();
		initModel(element);
	}

	public String getNamespacePrefix() {
		return namespacePrefix;
	}

	public void setNamespacePrefix(String namespacePrefix) {
		this.namespacePrefix = namespacePrefix;
	}

	public String getNamespaceUri() {
		return namespaceUri;
	}

	public void setNamespaceUri(String namespaceUri) {
		this.namespaceUri = namespaceUri;
	}

	public String getText() {
		return text;
	}
	
	public void setText(String text) {
		String oldText = this.text;
		this.text = text;
		firePropertyChange(this, getModelId(), oldText, text);
	}

	@SuppressWarnings("unchecked")
	public IXmlModel clone() throws CloneNotSupportedException {
		XmlModel clone = (XmlModel) super.clone();
		clone.element = (Element) element.clone();
		
		clone.attributeNameSpace = Utils.newMap();		
		for (String attribute : attributeNameSpace.keySet()) {
			Map<String, String> attrMap = (Map<String, String>) ((HashMap<String, String>)attributeNameSpace.get(attribute)).clone();
			clone.attributeNameSpace.put(attribute, attrMap);
		}
		return clone;
	}
	
	@SuppressWarnings({ "unchecked", "serial" })
	protected void populateToElement(Element element, IXmlModel model) {	
		if (Assert.isNotNullNotEmpty(model.getNamespaceUri())) { 
			String prefix = model.getNamespacePrefix();
			element.setNamespace(Namespace.getNamespace(Utils.getNotNullString(prefix), model.getNamespaceUri()));  
		} 
		
		//sort attribute
		List<String> props = Utils.newList();
		if (!model.getModelDefinition().isIgnoreCheck()) {
			props = model.getModelDefinition().getPropertySequence();
			for (Iterator<String> iter = props.iterator(); iter.hasNext();) {
				if (model.isUnset(iter.next())) {
					iter.remove();
				}
			}
		} else {
			props.addAll(new ArrayList<String>(model.getAllProperties()));
			Collections.sort(props);
		}
		
		//clear all attribute
		List<Attribute> attributes = element.getAttributes();
		for (Attribute attribute : attributes.toArray(new Attribute[0])) {
			element.removeAttribute(attribute);
		}
		
		//set attribute for root	
		for (String propertyName : props) {
			String propVal = model.getStringProperty(propertyName);
			if (Assert.isNotNullNotEmpty(propVal)) {
				if (! Assert.isNullOrEmpty(model.getPropertyNameSpace(propertyName))) {
					String uri = model.getPropertyNameSpace(propertyName).keySet().iterator().next();
					String prifix = model.getPropertyNameSpace(propertyName).get(uri);
					element.setAttribute(propertyName, propVal, Namespace.getNamespace(prifix, uri));
				} else {
					element.setAttribute(propertyName, propVal);
				}
			}
		}
		
		//remember the comment location
		Map<Integer, Content> comments = new HashMap<Integer, Content>();
		Filter commentAndElementFilter = new Filter(){
			public boolean matches(Object obj) {
				if (obj instanceof Comment
					|| obj instanceof Element) {
					return true;
				}
				return false;
			}
		};
		List<Object> contents = element.getContent(commentAndElementFilter);
		for (int i = 0; i < contents.size(); i++) {
			Object content = contents.get(i);
			if (content instanceof Comment) {
				comments.put(i, (Content) content);
			}
		}
		
		//remove all not Element content
		for (Object content : element.getContent().toArray()) {
			if (!(content instanceof Element)) {
				element.removeContent((Content)content);
			}
		}
		
		//set text
		boolean textSetted = false;
		if (Assert.isNotNullNotEmpty(model.getText())) {
			if (model.getModelDefinition() instanceof XmlModelDefinition) {
				if (((XmlModelDefinition)model.getModelDefinition()).isAllTextCData() ||
					((XmlModelDefinition)model.getModelDefinition()).isTextCData()) {
					element.addContent(new CDATA(model.getText()));
				} else {
					element.setText(model.getText());
				}
			} else {
				element.setText(model.getText());
			}
			textSetted = true;
		}
		
		//populate element
		List<Element> childrens = new ArrayList<Element>();		
		if (!((XmlModelDefinition)model.getModelDefinition()).isKeepChildOrder()) {
			for (ITreeModel child : model.getChild()){
				if (child instanceof IXmlModel) {
					Element childElement = ((IXmlModel)child).getElement();
					if (childElement == null) {
						childElement = new Element(child.getModelId());
					}
					
					populateToElement(childElement, (IXmlModel) child);
					
					if (element.getContent().contains(childElement)) {
						element.removeContent(childElement);
					}
					
					JDomUtils.insertChild(element, childElement, new String[0]);
					childrens.add(childElement);
				}
			}
		} else {
			for (String childName : model.getChildNames()) {
				for (ITreeModel child : model.getTreeChild(childName)){
					if (child instanceof IXmlModel) {
						Element childElement = ((IXmlModel)child).getElement();
						if (childElement == null) {
							childElement = new Element(childName);
						}
						populateToElement(childElement, (IXmlModel) child);
						String[] childrenSequence = ((XmlModel)model).getModelDefinition().getChildrenSequence();
						
						if (element.getContent().contains(childElement)) {
							element.removeContent(childElement);
						}
						JDomUtils.insertChild(element, childElement, childrenSequence); 
						childrens.add(childElement);
					}
				}
			}
		}
		
		//remove the not existed element
		List<Element> constansChildren = element.getChildren();
		for (Element child : constansChildren.toArray(new Element[0])) {
			if (!childrens.contains(child)) {
				element.removeContent(child);
			}
		}		

		contents = element.getContent(commentAndElementFilter);
		List<Integer> indexes = new ArrayList<Integer>();
		indexes.addAll(comments.keySet());
		Collections.sort(indexes);
		//add comment
		for (int i = indexes.size() - 1; i >= 0; i--) {
			Content content = comments.get(indexes.get(i));
			JDomUtils.insertChild(element, content, indexes.get(i) - i + (textSetted ? 1 : 0));
		}
	}
	
	public DocType getDocType() {
		return docType;
	}

	public void setDocType(DocType docType) {
		this.docType = docType;
	}
}
